/*******************************************************************************
 * The MOSES project, model based software engineering of networked embedded systems.
 * 
 * Copyright (C) 2010-2013, University of Applied Sciences Wildau,
 * Telematics Research Group, http://www.tm.th-wildau.de/research-group
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <http://www.gnu.org/licenses>.
 * 
 * Contributors:
 *     University of Applied Sciences Wildau - initial API and implementation
 * 
 * If you are interested in a commercial license please contact us via 
 * e-mail: ralf.vandenhouten@th-wildau.de.
 ******************************************************************************/
/*
 * generated by Xtext
 */
package de.thwildau.tm.moses.xabnf.validation;

import static de.thwildau.tm.moses.common.generator.JavaLanguageValidator.JAVA_CLASS_NAME_SUFFIX_REGEX;
import static de.thwildau.tm.moses.common.generator.JavaLanguageValidator.PACKAGE_NAME_REGEX;

import java.lang.reflect.Array;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EAttribute;
import org.eclipse.xtext.util.Pair;
import org.eclipse.xtext.util.Tuples;
import org.eclipse.xtext.validation.Check;

import com.google.inject.Inject;

import de.thwildau.tm.moses.common.generator.ILanguageValidator;
import de.thwildau.tm.moses.xabnf.ResourceFinder;
import de.thwildau.tm.moses.xabnf.xabnf.ClassBinding;
import de.thwildau.tm.moses.xabnf.xabnf.FieldBinding;
import de.thwildau.tm.moses.xabnf.xabnf.GeneratorModel;
import de.thwildau.tm.moses.xabnf.xabnf.Language;
import de.thwildau.tm.moses.xabnf.xabnf.RuleBinding;
import de.thwildau.tm.moses.xabnf.xabnf.XabnfPackage;

/**
 * Custom validation rules. see http://www.eclipse.org/Xtext/documentation.html#validation
 */
public class XabnfJavaValidator extends de.thwildau.tm.moses.xabnf.validation.AbstractXabnfJavaValidator {
    
    private static final String CLASS_PARAM_REGEX = "[" + PACKAGE_NAME_REGEX + "]*" + JAVA_CLASS_NAME_SUFFIX_REGEX + "[\\[\\]]*";
    private static final String METHOD_PARAMETER_REGEX = "(" + CLASS_PARAM_REGEX + ")[," + CLASS_PARAM_REGEX + "]*";
    private static final String METHOD_REGEX = "[a-zA-Z_][a-zA-Z0-9_]*\\(" + METHOD_PARAMETER_REGEX + "\\)";
    private static final Pattern JAVA_METHOD_NAME = Pattern.compile(METHOD_REGEX);

    @Inject
    private ILanguageValidator languageValidator;
    
    @Inject
    private ResourceFinder resourceFinder;
    
    @Check
    public void checkEncoding(GeneratorModel generatorModel) {
        String encoding = generatorModel.getEncoding();
        if(encoding != null) {
            try {
                boolean supported = Charset.isSupported(encoding);
                if(!supported) {
                    String message = "The encoding '" + encoding + "' is not supported.";
                    warning(message, generatorModel, XabnfPackage.eINSTANCE.getGeneratorModel_Encoding());
                }
            } catch(IllegalCharsetNameException e) {
                String message = "The encoding name '" + encoding + "' is invalid.";
                error(message, generatorModel, XabnfPackage.eINSTANCE.getGeneratorModel_Encoding());
            }
        }
    }

    @Check
    public void checkTargetRuleName(RuleBinding ruleBindung) {
        String targetRuleName = ruleBindung.getTargetRuleName();
        if (!languageValidator.validClassName(targetRuleName)) {
            EAttribute feature = XabnfPackage.eINSTANCE.getRuleBinding_TargetRuleName();
            error(targetRuleName + " is not a valid binding.", feature);
        }
    }

    @Check
    public void checkClassBinding(ClassBinding classBinding) {
        String className = classBinding.getClassName();
        EAttribute feature = XabnfPackage.eINSTANCE.getClassBinding_ClassName();
        if (languageValidator.isKeyword(className)) {
            error(className + " is a Keyword.", classBinding, feature);
        } else {
            ClassLoader cl = getClass().getClassLoader();
            try {
                Class<?> clazz = cl.loadClass(className);
                checkMethodName(classBinding, clazz);
            } catch (ClassNotFoundException e) {
                warning("The class '" + className + "' cannot be found in the classpath.", classBinding, feature);
            }
        }
    }

    /**
     * Checks the "methodName" of a {@link ClassBinding}. This method checks if given <code>class</code> declares
     * the method that is given in the "methodName" attribute of the {@link ClassBinding}.
     * 
     * @param classBinding the ClassBinding
     * @param clazz the Class object of which the method name belongs to
     * @see ClassBinding#getMethodName()
     */
    private void checkMethodName(ClassBinding classBinding, Class<?> clazz) {
        String fullMethodName = classBinding.getMethodName();
        if (fullMethodName != null) {
            // remove whitespaces
            fullMethodName = fullMethodName.replaceAll(" ", "");
            EAttribute feature = XabnfPackage.eINSTANCE.getClassBinding_MethodName();
            
            // use regular expression to check whether the method name is syntactically correct
            Matcher matcher = JAVA_METHOD_NAME.matcher(fullMethodName);
            if (matcher.matches()) {
                try {
                    // extract the Class objects of the param-string of the fullMethodName
                    Class<?>[] paramClasses = getMethodParameterClasses(classBinding, fullMethodName);
                    String methodName = fullMethodName.substring(0, fullMethodName.indexOf("("));
                    clazz.getDeclaredMethod(methodName, paramClasses);
                } catch (NoSuchMethodException e) {
                    String message = "The class " + clazz.getName() + " does not declare the method '" + fullMethodName
                            + "'.";
                    warning(message, classBinding, feature);
                }
            } else {
                warning("The declared methodName has an invalid format.", classBinding, feature);
            }
        }
    }

    /**
     * Returns the Class objects of the method parameters. For example <code>String#indexOf(String)</code> would return
     * {@link Class<java.lang.String>}.
     * 
     * @param classBinding the ClassBinding that is used to display possible warnings or errors
     * @param methodName the full method name
     * @return the Class objects of the method parameter
     */
    private Class<?>[] getMethodParameterClasses(ClassBinding classBinding, String methodName) {
        int startBracket = methodName.indexOf("(");
        int endBracket = methodName.indexOf(")");
        if (startBracket > 0 && endBracket > startBracket) {
            String paramString = methodName.substring(startBracket + 1, endBracket);
            if (!paramString.isEmpty()) {
                EAttribute feature = XabnfPackage.eINSTANCE.getClassBinding_MethodName();
                String[] classParams = paramString.split(",");
                Class<?>[] classes = new Class<?>[classParams.length];
                for (int i = 0; i < classParams.length; i++) {
                    String classParam = classParams[i];
                    try {
                        classes[i] = getClassObject(classParam);
                    } catch (ClassNotFoundException e) {
                        String message = "The method parameter class '" + classParam + "' was not found.";
                        warning(message, classBinding, feature);
                    }
                }
                return classes;
            }
        }
        return new Class<?>[0];
    }

    /**
     * Returns the class objects of the given String representation. The <code>classParam</code> can be:
     * <ul>
     * <li>a Java primitive (e.g. int, boolean, etc)</li>
     * <li>a full qualified class name (e.g. java.lang.String)</li>
     * <li>an qualified class name of an array type (e.g. java.lang.String[] or int[])</li>
     * </ul>
     * 
     * @param classParam the string representing the full qualified name of the class
     * @return the Class object
     * @throws ClassNotFoundException if the Class cannot be found in the class's Classloader
     */
    private Class<?> getClassObject(String classParam) throws ClassNotFoundException {
        Pair<String, Integer> arrayClassParameter = toArrayClassParameter(classParam);
        String className = arrayClassParameter.getFirst();
        Class<?> clazz = null;
        switch (className) {
            case "boolean":
                clazz = Boolean.TYPE;
                break;
            case "char":
                clazz = Character.TYPE;
                break;
            case "byte":
                clazz = Byte.TYPE;
                break;
            case "short":
                clazz = Short.TYPE;
                break;
            case "int":
                clazz = Integer.TYPE;
                break;
            case "long":
                clazz = Long.TYPE;
                break;
            case "float":
                clazz = Float.TYPE;
                break;
            case "double":
                clazz = Double.TYPE;
                break;
            default:
                clazz = getClass().getClassLoader().loadClass(className);
        }
        
        if(arrayClassParameter.getSecond() > 0) {
            clazz = Array.newInstance(clazz, arrayClassParameter.getSecond()).getClass();
        }
        return clazz; 
    }
    
    /**
     * Converts the given classParam into an array class parameter. This means e.g. 
     * <ul>
     * <li><code>java.lang.String</code> => "java.lang.String:0"</li>
     * <li><code>java.lang.String[]</code> => "java.lang.String:1"</li>
     * <li><code>java.lang.String[][]</code> => "java.lang.String:2"</li>
     * </ul>
     * 
     * The second parameter of the Pair object means if the given <code>classParam</code> declares
     * a array type. The value is the dimension of the the array. The first parameter of the 
     * Pair object will be the raw full qualified name (without []).
     *  
     * @param classParam the string representing the full qualified name of the class
     * @return
     */
    private Pair<String, Integer> toArrayClassParameter(String classParam) {
        int startBrackets = 0;
        int endBrackets = 0;
        String subString = null;
        for(int i=0; i<classParam.length(); i++) {
            char charAt = classParam.charAt(i);
            if(charAt == '[') {
                if(startBrackets == 0) {
                    subString = classParam.substring(0, i);
                }
                startBrackets++;
            }
            if(charAt == ']')
                endBrackets++;
        }
        
        subString = subString == null ? classParam : subString;
        int arrayLength = startBrackets == endBrackets ? startBrackets : 0;
        return Tuples.create(subString, arrayLength);   
    }

    @Check
    public void checkPackage(GeneratorModel generatorModel) {
        EAttribute feature = XabnfPackage.eINSTANCE.getGeneratorModel_Package();
        String packageName = generatorModel.getPackage();
        if (packageName == null) {
            warning("A package should be specified.", generatorModel, feature);
        } else if (!languageValidator.validPackageName(packageName)) {
            error("'" + packageName + "' is not a valid package name.", generatorModel, feature);
        }
    }

    @Check
    public void checkFile(GeneratorModel generatorModel) {
        URI abnfUri = resourceFinder.findAbnfUri(generatorModel);
        if(abnfUri == null) {
            warning("The file '" + generatorModel.getFile() + "' does not exist", generatorModel,
                    XabnfPackage.eINSTANCE.getGeneratorModel_File());
        }
    }

    @Check
    public void checkTargetName(FieldBinding fieldBinding) {
        String targetName = fieldBinding.getTargetName();
        if (!languageValidator.validFieldName(targetName)) {
            warning(targetName + " is not a valid field name.", fieldBinding,
                    XabnfPackage.eINSTANCE.getFieldBinding_TargetName());
        }
    }

    @Check
    public void checkRuleComplete(RuleBinding ruleBinding) {
        String targetRuleName = ruleBinding.getTargetRuleName();
        EList<FieldBinding> fieldBindings = ruleBinding.getFieldBindings();
        if (targetRuleName == null && (fieldBindings == null || fieldBindings.size() == 0)) {
            warning("A RuleBinding should not be empty", ruleBinding,
                    XabnfPackage.eINSTANCE.getRuleBinding_TargetRuleName());
        }
    }
    
    @Check
    public void checkLanguage(GeneratorModel generatorModel) {
        Language language = generatorModel.getLanguage();
        if(!language.equals(Language.JAVA)) {
            String message = "The language '" + language.getLiteral() + "' is currently not supported. Only Java is supported.";
            error(message, generatorModel, XabnfPackage.eINSTANCE.getGeneratorModel_Language());
        }
    }

    // @Check
    // public void checkGreetingStartsWithCapital(Greeting greeting) {
    // if (!Character.isUpperCase(greeting.getName().charAt(0))) {
    // warning("Name should start with a capital", MyDslPackage.Literals.GREETING__NAME);
    // }
    // }
}
